package org.apache.lucene.store;

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.io.IOException;

/** Base implementation class for buffered {@link IndexInput}. */
public abstract class BufferedIndexInput extends IndexInput {

    /** Default buffer size */
    public static final int BUFFER_SIZE = 1024;

    private int bufferSize = BUFFER_SIZE;

    protected byte[] buffer;

    private long bufferStart = 0; // position in file of buffer
    private int bufferLength = 0; // end of valid bytes
    private int bufferPosition = 0; // next byte to read

    @Override
    public byte readByte() throws IOException {
        if (bufferPosition >= bufferLength)
            refill();
        return buffer[bufferPosition++];
    }

    /** @deprecated please pass resourceDesc */
    @Deprecated
    public BufferedIndexInput() {
        this("anonymous BuffereIndexInput");
    }

    public BufferedIndexInput(String resourceDesc) {
        this(resourceDesc, BUFFER_SIZE);
    }

    /** Inits BufferedIndexInput with a specific bufferSize
     *  @deprecated please pass resourceDesc */
    @Deprecated
    public BufferedIndexInput(int bufferSize) {
        this("anonymous BuffereIndexInput", bufferSize);
    }

    /** Inits BufferedIndexInput with a specific bufferSize */
    public BufferedIndexInput(String resourceDesc, int bufferSize) {
        super(resourceDesc);
        checkBufferSize(bufferSize);
        this.bufferSize = bufferSize;
    }

    /** Change the buffer size used by this IndexInput */
    public void setBufferSize(int newSize) {
        assert buffer == null || bufferSize == buffer.length : "buffer=" + buffer + " bufferSize=" + bufferSize + " buffer.length=" + (buffer != null ? buffer.length : 0);
        if (newSize != bufferSize) {
            checkBufferSize(newSize);
            bufferSize = newSize;
            if (buffer != null) {
                // Resize the existing buffer and carefully save as
                // many bytes as possible starting from the current
                // bufferPosition
                byte[] newBuffer = new byte[newSize];
                final int leftInBuffer = bufferLength - bufferPosition;
                final int numToCopy;
                if (leftInBuffer > newSize)
                    numToCopy = newSize;
                else
                    numToCopy = leftInBuffer;
                System.arraycopy(buffer, bufferPosition, newBuffer, 0, numToCopy);
                bufferStart += bufferPosition;
                bufferPosition = 0;
                bufferLength = numToCopy;
                newBuffer(newBuffer);
            }
        }
    }

    protected void newBuffer(byte[] newBuffer) {
        // Subclasses can do something here
        buffer = newBuffer;
    }

    /** Returns buffer size.  @see #setBufferSize */
    public int getBufferSize() {
        return bufferSize;
    }

    private void checkBufferSize(int bufferSize) {
        if (bufferSize <= 0)
            throw new IllegalArgumentException("bufferSize must be greater than 0 (got " + bufferSize + ")");
    }

    @Override
    public void readBytes(byte[] b, int offset, int len) throws IOException {
        readBytes(b, offset, len, true);
    }

    @Override
    public void readBytes(byte[] b, int offset, int len, boolean useBuffer) throws IOException {

        if (len <= (bufferLength - bufferPosition)) {
            // the buffer contains enough data to satisfy this request
            if (len > 0) // to allow b to be null if len is 0...
                System.arraycopy(buffer, bufferPosition, b, offset, len);
            bufferPosition += len;
        } else {
            // the buffer does not have enough data. First serve all we've got.
            int available = bufferLength - bufferPosition;
            if (available > 0) {
                System.arraycopy(buffer, bufferPosition, b, offset, available);
                offset += available;
                len -= available;
                bufferPosition += available;
            }
            // and now, read the remaining 'len' bytes:
            if (useBuffer && len < bufferSize) {
                // If the amount left to read is small enough, and
                // we are allowed to use our buffer, do it in the usual
                // buffered way: fill the buffer and copy from it:
                refill();
                if (bufferLength < len) {
                    // Throw an exception when refill() could not read len bytes:
                    System.arraycopy(buffer, 0, b, offset, bufferLength);
                    throw new IOException("read past EOF");
                } else {
                    System.arraycopy(buffer, 0, b, offset, len);
                    bufferPosition = len;
                }
            } else {
                // The amount left to read is larger than the buffer
                // or we've been asked to not use our buffer -
                // there's no performance reason not to read it all
                // at once. Note that unlike the previous code of
                // this function, there is no need to do a seek
                // here, because there's no need to reread what we
                // had in the buffer.
                long after = bufferStart + bufferPosition + len;
                if (after > length())
                    throw new IOException("read past EOF");
                readInternal(b, offset, len);
                bufferStart = after;
                bufferPosition = 0;
                bufferLength = 0; // trigger refill() on read
            }
        }
    }

    @Override
    public int readInt() throws IOException {
        if (4 <= (bufferLength - bufferPosition)) {
            return ((buffer[bufferPosition++] & 0xFF) << 24) | ((buffer[bufferPosition++] & 0xFF) << 16) | ((buffer[bufferPosition++] & 0xFF) << 8) | (buffer[bufferPosition++] & 0xFF);
        } else {
            return super.readInt();
        }
    }

    @Override
    public long readLong() throws IOException {
        if (8 <= (bufferLength - bufferPosition)) {
            final int i1 = ((buffer[bufferPosition++] & 0xff) << 24) | ((buffer[bufferPosition++] & 0xff) << 16) | ((buffer[bufferPosition++] & 0xff) << 8) | (buffer[bufferPosition++] & 0xff);
            final int i2 = ((buffer[bufferPosition++] & 0xff) << 24) | ((buffer[bufferPosition++] & 0xff) << 16) | ((buffer[bufferPosition++] & 0xff) << 8) | (buffer[bufferPosition++] & 0xff);
            return (((long) i1) << 32) | (i2 & 0xFFFFFFFFL);
        } else {
            return super.readLong();
        }
    }

    @Override
    public int readVInt() throws IOException {
        if (5 <= (bufferLength - bufferPosition)) {
            byte b = buffer[bufferPosition++];
            int i = b & 0x7F;
            for (int shift = 7; (b & 0x80) != 0; shift += 7) {
                b = buffer[bufferPosition++];
                i |= (b & 0x7F) << shift;
            }
            return i;
        } else {
            return super.readVInt();
        }
    }

    @Override
    public long readVLong() throws IOException {
        if (9 <= bufferLength - bufferPosition) {
            byte b = buffer[bufferPosition++];
            long i = b & 0x7F;
            for (int shift = 7; (b & 0x80) != 0; shift += 7) {
                b = buffer[bufferPosition++];
                i |= (b & 0x7FL) << shift;
            }
            return i;
        } else {
            return super.readVLong();
        }
    }

    private void refill() throws IOException {
        long start = bufferStart + bufferPosition;
        long end = start + bufferSize;
        if (end > length()) // don't read past EOF
            end = length();
        int newLength = (int) (end - start);
        if (newLength <= 0)
            throw new IOException("read past EOF");

        if (buffer == null) {
            newBuffer(new byte[bufferSize]); // allocate buffer lazily
            seekInternal(bufferStart);
        }
        readInternal(buffer, 0, newLength);
        bufferLength = newLength;
        bufferStart = start;
        bufferPosition = 0;
    }

    /** Expert: implements buffer refill.  Reads bytes from the current position
     * in the input.
     * @param b the array to read bytes into
     * @param offset the offset in the array to start storing bytes
     * @param length the number of bytes to read
     */
    protected abstract void readInternal(byte[] b, int offset, int length) throws IOException;

    @Override
    public long getFilePointer() {
        return bufferStart + bufferPosition;
    }

    @Override
    public void seek(long pos) throws IOException {
        if (pos >= bufferStart && pos < (bufferStart + bufferLength))
            bufferPosition = (int) (pos - bufferStart); // seek within buffer
        else {
            bufferStart = pos;
            bufferPosition = 0;
            bufferLength = 0; // trigger refill() on read()
            seekInternal(pos);
        }
    }

    /** Expert: implements seek.  Sets current position in this file, where the
     * next {@link #readInternal(byte[],int,int)} will occur.
     * @see #readInternal(byte[],int,int)
     */
    protected abstract void seekInternal(long pos) throws IOException;

    @Override
    public Object clone() {
        BufferedIndexInput clone = (BufferedIndexInput) super.clone();

        clone.buffer = null;
        clone.bufferLength = 0;
        clone.bufferPosition = 0;
        clone.bufferStart = getFilePointer();

        return clone;
    }

    /**
     * Flushes the in-memory bufer to the given output, copying at most
     * <code>numBytes</code>.
     * <p>
     * <b>NOTE:</b> this method does not refill the buffer, however it does
     * advance the buffer position.
     * 
     * @return the number of bytes actually flushed from the in-memory buffer.
     */
    protected int flushBuffer(IndexOutput out, long numBytes) throws IOException {
        int toCopy = bufferLength - bufferPosition;
        if (toCopy > numBytes) {
            toCopy = (int) numBytes;
        }
        if (toCopy > 0) {
            out.writeBytes(buffer, bufferPosition, toCopy);
            bufferPosition += toCopy;
        }
        return toCopy;
    }

    @Override
    public void copyBytes(IndexOutput out, long numBytes) throws IOException {
        assert numBytes >= 0 : "numBytes=" + numBytes;

        while (numBytes > 0) {
            if (bufferLength == bufferPosition) {
                refill();
            }
            numBytes -= flushBuffer(out, numBytes);
        }
    }

}
